﻿/* ----------------------------------------------------------------

	This file is part of TCP/IP Message Manager
	
	Copyright (C) 2014 Aleksejs Belezjaks http://alexbelezjaks.com
	
	TCP/IP Message Manager is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    TCP/IP Message Manager is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
	
	--------------------------------------------------------------- */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

namespace MsgManager
{
    /// <summary>
    /// Message class. Used to store and send messages beetwen client.
    /// Also converts message from bytes to to bytes.
    /// </summary>
    public class Message
    {
        private const byte CURRENT_VERSION = 1;        

        private string messageType;
        public string MessageType
        {
            get { return messageType; }
        }

        private byte version;
        public byte Version
        {
            get { return version; }
        }

        private UInt32 checksum;
        /// <summary>
        /// Checksum is sum of all message data/payload bytes.
        /// </summary>
        public UInt32 Checksum
        {
            get { return checksum; }
        }

        private string data;
        /// <summary>
        /// Message data/payload. Supports only ASCII.
        /// </summary>
        public string Data
        {
            get { return data; }
        }

        // Byte array should NOT include Magic number
        private byte[] bytes;
        public byte[] Bytes
        {
            get { return bytes; }
        }

//----------------------------- CONSTRUCTORS -----------------------------//

        /// <summary>
        /// For internal use only
        /// </summary>
        private Message()
        {

        }
        private Message(byte[] bytes, bool dummy)
        {
            this.bytes = bytes;
        }

        /// <summary>
        /// Creates message from array of bytes as data/payload
        /// </summary>
        /// <param name="bytes">Array of bytes</param>
        public Message(byte[] bytes)
        {
            Message msg = BytesToMessage(bytes);

            this.messageType = msg.MessageType;
            this.version = msg.Version;
            this.checksum = msg.Checksum;
            this.data = msg.Data;
            this.bytes = bytes;     
        }

        /// <summary>
        /// Creates message to send over TCP/IP
        /// </summary>
        /// <param name="type">Type of message</param>
        /// <param name="data">Message data (could be null)</param>
        public Message(string type, string data)
        {
            this.messageType = type;
            this.version = CURRENT_VERSION;
            this.data = data ?? "";
            this.checksum = CreateChecksum(Data);
            this.bytes = MessageToBytes(this);
        }

//---------------------------- BYTE LEVEL STUFF ---------------------------//
//  Handle with care ! Or do not touch at all since it's working somehow !  

        private const int version_len = 1;
        private const int command_len = 12;
        private const int checksum_len = 4;
        private const int payloadlen_len = 4;

        private const int version_pos = 0;
        private const int command_pos = version_pos + version_len;
        private const int checksum_pos = command_pos + command_len;
        private const int payloadlen_pos = checksum_pos + checksum_len;
        private const int payload_pos = payloadlen_pos + payloadlen_len;

        /// <summary>
        /// Converts message to bytes data
        /// </summary>
        /// <param name="message"></param>
        /// <returns></returns>
        private static byte[] MessageToBytes(Message message)
        {
            int payload_len = message.Data.Length;

            // Creating array to store all bytes
            byte[] result = new byte[version_len + command_len + checksum_len + payloadlen_len + payload_len];

            // Version
            result[version_pos] = CURRENT_VERSION;

            // Command
            if (message.MessageType.ToString().Length > 12) throw new Exception("Message type is too long");
            byte[] cmd = Encoding.ASCII.GetBytes(message.MessageType.ToString());
            Array.Copy(cmd, 0, result, command_pos, cmd.Length);

            // Checksum
            byte[] cksum = UInt32ToBytes(message.Checksum);
            Array.Copy(cksum, 0, result, checksum_pos, cksum.Length);

            // Payload Length
            byte[] paylen = UInt32ToBytes((UInt32)message.Data.Length);
            Array.Copy(paylen, 0, result, payloadlen_pos, paylen.Length);

            // Payload
            byte[] payload = Encoding.ASCII.GetBytes(message.Data);
            Array.Copy(payload, 0, result, payload_pos, payload.Length);

            return result;
        }

        /// <summary>
        /// Creates message from bytes data
        /// </summary>
        /// <param name="bytes"></param>
        /// <returns></returns>
        private static Message BytesToMessage(byte[] bytes)
        {
            // Parse data from bytes array
            Message result = new Message();
            try
            {   
                // Check if we have enough bytes
                int min_len = version_len + command_len + checksum_len + payloadlen_len;
                if (bytes.Length < min_len) throw new Exception("Message is too short to be processed");
                result.bytes = bytes;

                // Version
                byte version = bytes[version_pos];
                if (version != CURRENT_VERSION) throw new Exception("Version number does not match current version");
                result.version = version;

                // Command
                byte[] cmd = new byte[12];
                Array.Copy(bytes, command_pos, cmd, 0, command_len);
                byte[] cmd_trimmed = Trim(cmd, 0);
                if (cmd_trimmed == null) throw new Exception("Command string wrong format");
                string command = Encoding.ASCII.GetString(cmd_trimmed);
                result.messageType = command;

                // Checksum
                byte[] cksum = new byte[4];
                Array.Copy(bytes, checksum_pos, cksum, 0, checksum_len);
                UInt32 checksum = BytesToUInt32(cksum);
                result.checksum = checksum;

                // Payload Length
                byte[] paylen = new byte[4];
                Array.Copy(bytes, payloadlen_pos, paylen, 0, payloadlen_len);
                UInt32 payload_len = BytesToUInt32(paylen);
                
                // Payload
                int remaining_len = bytes.Length - payload_pos;
                if (remaining_len < 0) throw new Exception("Holy crap, something strange is happening !");
                if ((UInt32)remaining_len != payload_len) throw new Exception("Payload length mismatch");

                byte[] payload = new byte[payload_len];
                Array.Copy(bytes, payload_pos, payload, 0, payload_len);
                UInt32 test_checksum = CreateChecksum(payload);
                if (checksum != test_checksum) throw new Exception("Checksum mismatch");

                string data = Encoding.ASCII.GetString(payload);
                result.data = data; 
            }
            catch (Exception ex)
            {
                throw new ArgumentException("Message is wrong format", ex);
            }
            return result;
        }

        /// <summary>
        /// Converts 32bit unsigned integer into array of four bytes
        /// </summary>
        /// <param name="i"></param>
        /// <returns></returns>
        private static byte[] UInt32ToBytes(UInt32 i)
        {
            byte[] intBytes = BitConverter.GetBytes(i);
            if (BitConverter.IsLittleEndian)
                Array.Reverse(intBytes);
            return intBytes;
        }

        /// <summary>
        /// Converts array of four bytes into 32bit unsigned integer
        /// </summary>
        /// <param name="bytes"></param>
        /// <returns></returns>
        private static UInt32 BytesToUInt32(byte[] bytes)
        {
            if (bytes.Length != 4) throw new Exception("Number of bytes should be four !");

            if (BitConverter.IsLittleEndian)
                Array.Reverse(bytes);
            return BitConverter.ToUInt32(bytes, 0);
        }

        /// <summary>
        /// Trims bytes which are enclosed by white spaces
        /// </summary>
        /// <param name="bytes"></param>
        /// <param name="whilespace"></param>
        /// <returns></returns>
        private static byte[] Trim(byte[] bytes, byte whilespace)
        {
            int start = -1;
            int end = -1;
            for (int i = 0; i < bytes.Length; i++)
            {
                if (start == -1)
                {
                    if (bytes[i] != whilespace) start = i;
                }
                else if (end == -1)
                {
                    if (bytes[i] == whilespace)
                    {
                        end = i - 1;
                        break;
                    }
                    if (bytes[i] != whilespace && i == bytes.Length - 1)
                    {
                        end = i;
                        break;
                    }
                }
            }

            if (start == -1 || end == -1) return null;

            int len = (end - start) + 1;
            byte[] result = new byte[len];
            Array.Copy(bytes, start, result, 0, len);
            return result;
        }

        /// <summary>
        /// Creates checksum for message data in string format
        /// Checksum is sum of all bytes (string is ASCII format to 1 byte per char
        /// </summary>
        /// <param name="message">message data/payload</param>
        /// <returns>returns checksum</returns>
        private static UInt32 CreateChecksum(string message)
        {
            UInt32 result = 0;

            byte[] bytes = Encoding.ASCII.GetBytes(message);   

            foreach (byte b in bytes)
            {
                result += b;
            }

            return result;
        }

        /// <summary>
        /// Creates checksum for message data in byte array format
        /// Checksum is sum of all bytes (string is ASCII format to 1 byte per char
        /// </summary>
        /// <param name="message">message data bytes</param>
        /// <returns>checksum</returns>
        private static UInt32 CreateChecksum(byte[] message)
        {
            UInt32 result = 0;

            foreach (byte b in message)
            {
                result += b;
            }

            return result;
        }

        private static Random random = new Random();
        /// <summary>
        /// Returns message with random bytes for testing.
        /// </summary>
        /// <returns></returns>
        public static Message RandomMessage()
        {
            int len = random.Next(0, 10000000);
            byte[] bytes = new byte[len];
            random.NextBytes(bytes);
            return new Message(bytes, true);
        }

        /// <summary>
        /// Converts message to string in format "type":"data"
        /// </summary>
        /// <returns>return string representation of message</returns>
        public override string ToString()
        {
            return String.Format("{0}: {1}", messageType.ToString(), data);
        }
    }
}
